var utils = require('./utils')
var event = require('./event')
var File = require('./file')
var Chunk = require('./chunk')

var version = '__VERSION__'

var isServer = typeof window === 'undefined'

// ie10+
var ie10plus = isServer ? false : window.navigator.msPointerEnabled
var support = (function () {
    if (isServer) {
        return false
    }
    var sliceName = 'slice'
    var _support = utils.isDefined(window.File) && utils.isDefined(window.Blob) &&
        utils.isDefined(window.FileList)
    var bproto = null
    if (_support) {
        bproto = window.Blob.prototype
        utils.each(['slice', 'webkitSlice', 'mozSlice'], function (n) {
            if (bproto[n]) {
                sliceName = n
                return false
            }
        })
        _support = !!bproto[sliceName]
    }
    if (_support) Uploader.sliceName = sliceName
    bproto = null
    return _support
})()

var supportDirectory = (function () {
    if (isServer) {
        return false
    }
    var input = window.document.createElement('input')
    input.type = 'file'
    var sd = 'webkitdirectory' in input || 'directory' in input
    input = null
    return sd
})()

function Uploader(opts) {
    this.support = support
    /* istanbul ignore if */
    if (!this.support) {
        return
    }
    this.supportDirectory = supportDirectory
    utils.defineNonEnumerable(this, 'filePaths', {})
    this.opts = utils.extend({}, Uploader.defaults, opts || {})

    this.preventEvent = utils.bind(this._preventEvent, this)

    File.call(this, this)
}

/**
 * Default read function using the webAPI
 *
 * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk)
 *
 */
var webAPIFileRead = function (fileObj, fileType, startByte, endByte, chunk) {
    chunk.readFinished(fileObj.file[Uploader.sliceName](startByte, endByte, fileType))
}

Uploader.version = version

Uploader.defaults = {
    chunkSize: 1024 * 1024,
    chunkEnable:true,
    forceChunkSize: false,
    simultaneousUploads: 3,
    singleFile: false,
    fileParameterName: 'file',
    progressCallbacksInterval: 500,
    speedSmoothingFactor: 0.1,
    query: {},
    headers: {},
    withCredentials: false,
    preprocess: null,
    method: 'multipart',
    testMethod: 'GET',
    uploadMethod: 'POST',
    prioritizeFirstAndLastChunk: false,
    allowDuplicateUploads: false,
    target: '/',
    testChunks: true,
    generateUniqueIdentifier: null,
    maxChunkRetries: 0,
    chunkRetryInterval: null,
    permanentErrors: [404, 415, 500, 501],
    successStatuses: [200, 201, 202],
    onDropStopPropagation: false,
    initFileFn: null,
    readFileFn: webAPIFileRead,
    checkChunkUploadedByResponse: null,
    initialPaused: false,
    processResponse: function (response, cb) {
        cb(null, response)
    },
    processParams: function (params) {
        return params
    }
}

Uploader.utils = utils
Uploader.event = event
Uploader.File = File
Uploader.Chunk = Chunk

// inherit file
Uploader.prototype = utils.extend({}, File.prototype)
// inherit event
utils.extend(Uploader.prototype, event)
utils.extend(Uploader.prototype, {

    constructor: Uploader,

    _trigger: function (name) {
        var args = utils.toArray(arguments)
        var preventDefault = !this.trigger.apply(this, arguments)
        if (name !== 'catchAll') {
            args.unshift('catchAll')
            preventDefault = !this.trigger.apply(this, args) || preventDefault
        }
        return !preventDefault
    },

    _triggerAsync: function () {
        var args = arguments
        utils.nextTick(function () {
            this._trigger.apply(this, args)
        }, this)
    },
    addFiles: function (files, evt) {
        var _files = []
        var oldFileListLen = this.fileList.length
        utils.each(files, function (file) {
            // Uploading empty file IE10/IE11 hangs indefinitely
            // Directories have size `0` and name `.`
            // Ignore already added files if opts.allowDuplicateUploads is set to false
            if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
                var uniqueIdentifier = this.generateUniqueIdentifier(file)
                if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
                    var _file = new File(this, file, this)
                    _file.uniqueIdentifier = uniqueIdentifier
                    if (this._trigger('fileAdded', _file, evt)) {
                        _files.push(_file)
                    } else {
                        File.prototype.removeFile.call(this, _file)
                    }
                }else{
                    this._trigger('fileDuplicate', file)
                }
            }
        }, this)
        // get new fileList
        var newFileList = this.fileList.slice(oldFileListLen)
        if (this._trigger('filesAdded', _files, newFileList, evt)) {
            utils.each(_files, function (file) {
                if (this.opts.singleFile && this.files.length > 0) {
                    this.removeFile(this.files[0])
                }
                this.files.push(file)
            }, this)
            this._trigger('filesSubmitted', _files, newFileList, evt)
        } else {
            utils.each(newFileList, function (file) {
                File.prototype.removeFile.call(this, file)
            }, this)
        }
    },

    addFile: function (file, evt) {
        this.addFiles([file], evt)
    },

    cancel: function () {
        for (var i = this.fileList.length - 1; i >= 0; i--) {
            this.fileList[i].cancel()
        }
    },

    removeFile: function (file) {
        File.prototype.removeFile.call(this, file)
        this._trigger('fileRemoved', file)
    },

    generateUniqueIdentifier: function (file) {
        var custom = this.opts.generateUniqueIdentifier
        if (utils.isFunction(custom)) {
            return custom(file)
        }
        /* istanbul ignore next */
        // Some confusion in different versions of Firefox
        var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name
        /* istanbul ignore next */
        return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')
    },

    getFromUniqueIdentifier: function (uniqueIdentifier) {
        var ret = false
        utils.each(this.files, function (file) {
            if (file.uniqueIdentifier === uniqueIdentifier) {
                ret = file
                return false
            }
        })
        return ret
    },

    uploadNextChunk: function (preventEvents) {
        var found = false
        var pendingStatus = Chunk.STATUS.PENDING
        var checkChunkUploaded = this.uploader.opts.checkChunkUploadedByResponse
        if (this.opts.prioritizeFirstAndLastChunk) {
            utils.each(this.files, function (file) {
                if (file.paused) {
                    return
                }
                if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
                    // waiting for current file's first chunk response
                    return
                }
                if (file.chunks.length && file.chunks[0].status() === pendingStatus) {
                    file.chunks[0].send()
                    found = true
                    return false
                }
                if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === pendingStatus) {
                    file.chunks[file.chunks.length - 1].send()
                    found = true
                    return false
                }
            })
            if (found) {
                return found
            }
        }

        // Now, simply look for the next, best thing to upload
        utils.each(this.files, function (file) {
            if (!file.paused) {
                if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
                    // waiting for current file's first chunk response
                    return
                }
                utils.each(file.chunks, function (chunk) {
                    if (chunk.status() === pendingStatus) {
                        chunk.send()
                        found = true
                        return false
                    }
                })
            }
            if (found) {
                return false
            }
        })
        if (found) {
            return true
        }

        // The are no more outstanding chunks to upload, check is everything is done
        var outstanding = false
        utils.each(this.files, function (file) {
            if (!file.isComplete()) {
                outstanding = true
                return false
            }
        })
        // should check files now
        // if now files in list
        // should not trigger complete event
        if (!outstanding && !preventEvents && this.files.length) {
            // All chunks have been uploaded, complete
            this._triggerAsync('complete')
        }
        return outstanding
    },

    upload: function (preventEvents) {
        // Make sure we don't start too many uploads at once
        var ret = this._shouldUploadNext()
        if (ret === false) {
            return
        }
        !preventEvents && this._trigger('uploadStart')
        var started = false
        for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
            started = this.uploadNextChunk(!preventEvents) || started
            if (!started && preventEvents) {
                // completed
                break
            }
        }
        if (!started && !preventEvents) {
            this._triggerAsync('complete')
        }
    },

    /**
     * should upload next chunk
     * @function
     * @returns {Boolean|Number}
     */
    _shouldUploadNext: function () {
        var num = 0
        var should = true
        var simultaneousUploads = this.opts.simultaneousUploads
        var uploadingStatus = Chunk.STATUS.UPLOADING
        utils.each(this.files, function (file) {
            utils.each(file.chunks, function (chunk) {
                if (chunk.status() === uploadingStatus) {
                    num++
                    if (num >= simultaneousUploads) {
                        should = false
                        return false
                    }
                }
            })
            return should
        })
        // if should is true then return uploading chunks's length
        return should && num
    },

    /**
     * Assign a browse action to one or more DOM nodes.
     * @function
     * @param {Element|Array.<Element>} domNodes
     * @param {boolean} isDirectory Pass in true to allow directories to
     * @param {boolean} singleFile prevent multi file upload
     * @param {Object} attributes set custom attributes:
     *  http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
     *  eg: accept: 'image/*'
     * be selected (Chrome only).
     */
    assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
        if (typeof domNodes.length === 'undefined') {
            domNodes = [domNodes]
        }

        utils.each(domNodes, function (domNode) {
            var input
            if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
                input = domNode
            } else {
                input = document.createElement('input')
                input.setAttribute('type', 'file')
                // display:none - not working in opera 12
                utils.extend(input.style, {
                    visibility: 'hidden',
                    position: 'absolute',
                    width: '1px',
                    height: '1px'
                })
                // for opera 12 browser, input must be assigned to a document
                domNode.appendChild(input)
                // https://developer.mozilla.org/en/using_files_from_web_applications)
                // event listener is executed two times
                // first one - original mouse click event
                // second - input.click(), input is inside domNode
                domNode.addEventListener('click', function (e) {
                    if (domNode.tagName.toLowerCase() === 'label') {
                        return
                    }
                    input.click()
                }, false)
            }
            if (!this.opts.singleFile && !singleFile) {
                input.setAttribute('multiple', 'multiple')
            }
            if (isDirectory) {
                input.setAttribute('webkitdirectory', 'webkitdirectory')
            }
            attributes && utils.each(attributes, function (value, key) {
                input.setAttribute(key, value)
            })
            // When new files are added, simply append them to the overall list
            var that = this
            input.addEventListener('change', function (e) {
                that._trigger(e.type, e)
                if (e.target.value) {
                    that.addFiles(e.target.files, e)
                    e.target.value = ''
                }
            }, false)
        }, this)
    },

    onDrop: function (evt) {
        this._trigger(evt.type, evt)
        if (this.opts.onDropStopPropagation) {
            evt.stopPropagation()
        }
        evt.preventDefault()
        this._parseDataTransfer(evt.dataTransfer, evt)
    },

    _parseDataTransfer: function (dataTransfer, evt) {
        if (dataTransfer.items && dataTransfer.items[0] &&
            dataTransfer.items[0].webkitGetAsEntry) {
            this.webkitReadDataTransfer(dataTransfer, evt)
        } else {
            this.addFiles(dataTransfer.files, evt)
        }
    },

    webkitReadDataTransfer: function (dataTransfer, evt) {
        var self = this
        var queue = dataTransfer.items.length
        var files = []
        utils.each(dataTransfer.items, function (item) {
            var entry = item.webkitGetAsEntry()
            if (!entry) {
                decrement()
                return
            }
            if (entry.isFile) {
                // due to a bug in Chrome's File System API impl - #149735
                fileReadSuccess(item.getAsFile(), entry.fullPath)
            } else {
                readDirectory(entry.createReader())
            }
        })

        function readDirectory(reader) {
            reader.readEntries(function (entries) {
                if (entries.length) {
                    queue += entries.length
                    utils.each(entries, function (entry) {
                        if (entry.isFile) {
                            var fullPath = entry.fullPath
                            entry.file(function (file) {
                                fileReadSuccess(file, fullPath)
                            }, readError)
                        } else if (entry.isDirectory) {
                            readDirectory(entry.createReader())
                        }
                    })
                    readDirectory(reader)
                } else {
                    decrement()
                }
            }, readError)
        }

        function fileReadSuccess(file, fullPath) {
            // relative path should not start with "/"
            file.relativePath = fullPath.substring(1)
            files.push(file)
            decrement()
        }

        function readError(fileError) {
            throw fileError
        }

        function decrement() {
            if (--queue === 0) {
                self.addFiles(files, evt)
            }
        }
    },

    _assignHelper: function (domNodes, handles, remove) {
        if (typeof domNodes.length === 'undefined') {
            domNodes = [domNodes]
        }
        var evtMethod = remove ? 'removeEventListener' : 'addEventListener'
        utils.each(domNodes, function (domNode) {
            utils.each(handles, function (handler, name) {
                domNode[evtMethod](name, handler, false)
            }, this)
        }, this)
    },

    _preventEvent: function (e) {
        utils.preventEvent(e)
        this._trigger(e.type, e)
    },

    /**
     * Assign one or more DOM nodes as a drop target.
     * @function
     * @param {Element|Array.<Element>} domNodes
     */
    assignDrop: function (domNodes) {
        this._onDrop = utils.bind(this.onDrop, this)
        this._assignHelper(domNodes, {
            dragover: this.preventEvent,
            dragenter: this.preventEvent,
            dragleave: this.preventEvent,
            drop: this._onDrop
        })
    },

    /**
     * Un-assign drop event from DOM nodes
     * @function
     * @param domNodes
     */
    unAssignDrop: function (domNodes) {
        this._assignHelper(domNodes, {
            dragover: this.preventEvent,
            dragenter: this.preventEvent,
            dragleave: this.preventEvent,
            drop: this._onDrop
        }, true)
        this._onDrop = null
    }
})

module.exports = Uploader
